﻿
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using System.Threading;
using Ninja.WebSockets;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Text;
using System.Linq;
using StackExchange.Redis;
using System.Globalization;
using Drive.LiveExcelHost.Services;
using System.Collections.Concurrent;
using Drive.LiveExcelHost.Models;
using Newtonsoft.Json;
using Drive.LiveExcelHost.Mvc;
using Drive.LiveExcelHost.Controllers;
using Drive.LiveChat.Server.Controllers;
using Drive.LiveChat.Server.Models;
using Newtonsoft.Json.Linq;

namespace Drive.LiveExcelHost
{
    public class WebSocketServer : IDisposable
    {
        private TcpListener _listener;
        private bool _isDisposed = false;
        ILogger _logger;
        private readonly IWebSocketServerFactory _webSocketServerFactory;
        private readonly ILoggerFactory _loggerFactory;
        private readonly HashSet<string> _supportedSubProtocols;
        // const int BUFFER_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
        const int BUFFER_SIZE = 4 * 1024 * 1024; // 4MB

        public WebSocketServer(IWebSocketServerFactory webSocketServerFactory, ILoggerFactory loggerFactory, IList<string> supportedSubProtocols = null)
        {
            _logger = loggerFactory.CreateLogger<WebSocketServer>();
            _webSocketServerFactory = webSocketServerFactory;
            _loggerFactory = loggerFactory;
            _supportedSubProtocols = new HashSet<string>(supportedSubProtocols ?? new string[0]);
        }

        private void ProcessTcpClient(TcpClient tcpClient, CancellationToken stoppingToken)
        {
            Task.Run(() => ProcessTcpClientAsync(tcpClient, stoppingToken));
        }

        private string GetSubProtocol(IList<string> requestedSubProtocols)
        {
            foreach (string subProtocol in requestedSubProtocols)
            {
                // match the first sub protocol that we support (the client should pass the most preferable sub protocols first)
                if (_supportedSubProtocols.Contains(subProtocol))
                {
                    _logger.LogInformation($"Http header has requested sub protocol {subProtocol} which is supported");

                    return subProtocol;
                }
            }

            if (requestedSubProtocols.Count > 0)
            {
                _logger.LogWarning($"Http header has requested the following sub protocols: {string.Join(", ", requestedSubProtocols)}. There are no supported protocols configured that match.");
            }

            return null;
        }


        private async Task ProcessTcpClientAsync(TcpClient tcpClient, CancellationToken stoppingToken)
        {
            try
            {
                if (_isDisposed)
                {
                    return;
                }

                // this worker thread stays alive until either of the following happens:
                // Client sends a close conection request OR
                // An unhandled exception is thrown OR
                // The server is disposed
                //_logger.LogInformation("Server: Connection opened. Reading Http header from stream");

                // get a secure or insecure stream
                Stream stream = tcpClient.GetStream();
                WebSocketHttpContext context = await _webSocketServerFactory.ReadHttpHeaderFromStreamAsync(stream);
                var request = new Request(stream, context.HttpHeader);
                if (request.isBadRequest)
                {
                    return;
                }
                request.HostIpAddress = (tcpClient.Client.RemoteEndPoint as IPEndPoint).Address.ToString();
                HttpContext.Current = new HttpContext(request);
                var response = HttpContext.Current.Response;
                response.Headers.Add("Access-Control-Allow-Origin", request.Origin);
                response.Headers.Add("Access-Control-Allow-Methods", "*");
                response.Headers.Add("Access-Control-Allow-Credentials", "true");
                response.Headers.Add("Access-Control-Allow-Headers", "*");
                if (request.Path == null)
                    return;
                var sid = new Regex("sid=(.+?)$").Match(request.PathAndQuery).Groups[1].Value;
                var uid = request.Cookies.ContainsKey("uid") ? request.Cookies["uid"] : "";
                var isAgent = request.Path == "/workbench/";
                if (isAgent) { 
                    uid = new Regex("agent_uid=(.+?)[&|$]").Match(request.PathAndQuery).Groups[1].Value;
                    uid = WebUtility.UrlDecode(uid);
                }
                if (context.IsWebSocketRequest)
                {
                    VisitorInfo visitor = SocketIoBootstrapper.DecodeVisitorInfo(uid);
                    if (visitor == null)
                        return;
                    string subProtocol = GetSubProtocol(context.WebSocketRequestedProtocols);
                    var options = new WebSocketServerOptions() { KeepAliveInterval = TimeSpan.FromSeconds(30), SubProtocol = subProtocol };
                    //_logger.LogInformation("Http header has requested an upgrade to Web Socket protocol. Negotiating Web Socket handshake");

                    WebSocket webSocket = await _webSocketServerFactory.AcceptWebSocketAsync(context, options);
                    await SocketIoBootstrapper.ProcessWebSocketRequest(visitor, request, webSocket, stoppingToken);
                    return;
                }
                else if (request.Method == "OPTIONS")
                {
                    response.Flush();
                }
                else if (request.Path.StartsWith("/api"))
                {
                    var controller = new Drive.LiveExcelHost.Controllers.SpreadsheetController();


                    try
                    {
                        controller.ProcessRequest();

                    }
                    catch (FileNotFoundException)
                    {
                        response.StatusCode = 404;
                    }
                    catch (Exception ex)
                    {
                        response.StatusCode = 500;
                        response.WriteString(ex.Message + "\r\n" + ex.StackTrace);
                    }
                    finally
                    {
                        response.Flush();
                    }
                }
                else if (request.Path.StartsWith("/initvisit"))
                {
                    try
                    {
                        using (var controller = new VisitorChatController())
                        {
                            var d = JsonConvert.DeserializeObject<InitVisitRequest>(request.Payload);
                            var visitor = SocketIoBootstrapper.DecodeVisitorInfo(d.uid);
                            var visitorId = visitor?.VisitorId ?? 0;
                            controller.AddVisit(ref visitorId, d.channelId, d.url, d.referer, d.searchengine, d.searchkw);
                            visitor = new VisitorInfo { VisitorId = visitorId, isAgent = false };
                            d.uid = SocketIoBootstrapper.EncodeVisitorInfo(visitor);
                            response.WriteJson(new { uid = d.uid });
                            //response.Cookies.Add(new Cookie("uid", d.uid, "/") { HttpOnly = false });
                        }
                    }
                    catch (Exception ex)
                    {
                        response.WriteJson(new { code = -1, msg = "service unvailable", error = ex.Message });
                    }
                }
                else if (request.Path.StartsWith("/app/"))
                {
                    var controller = new AgentChatController();
                    try
                    {
                        controller.ProcessRequest();
                    }
                    catch (FileNotFoundException)
                    {
                        response.StatusCode = 404;
                    }
                    catch (Exception ex)
                    {
                        response.StatusCode = 500;
                        response.WriteString(ex.Message + "\r\n" + ex.StackTrace);
                    }
                    finally
                    {
                        response.Flush();
                        controller.Dispose();
                    }
                }
                else
                {
                    var visitor = SocketIoBootstrapper.DecodeVisitorInfo(uid);
                    if (visitor != null)
                    {
                        SocketIoBootstrapper.TryUpgrade(visitor, ref sid, request, response, stoppingToken);
                        response.Cookies.Add(new Cookie("io", sid, "/") { HttpOnly = true });
                        response.Cookies.Add(new Cookie(visitor.cookieName, uid, "/") { HttpOnly = false });
                    }
                }
                await response.FlushAsync();
                stream.WriteLine();
                stream.Flush();
                stream.Close();
            }
            catch (ObjectDisposedException ex)
            {
                // do nothing. This will be thrown if the Listener has been stopped
            }
            catch (Exception ex)
            {
                //var s = ex.InnerException as SocketException; ;
                //if(s!=null && s.SocketErrorCode== SocketError.ConnectionAborted)
                //{
                //    return;
                //}
                _logger.LogError(ex.ToString());
            }
            finally
            {
                try
                {
                    tcpClient.Client.Close();
                    tcpClient.Close();
                }
                catch (Exception ex)
                {
                    _logger.LogError($"Failed to close TCP connection: {ex}");
                }
            }
        }


        public async Task StartAsync(int port, CancellationToken stoppingToken)
        {
            try
            {
                IPAddress localAddress = IPAddress.Any;
                _listener = new TcpListener(localAddress, port);
                _listener.Start();
                _logger.LogInformation($"Server started listening on port {port}");
                while (!stoppingToken.IsCancellationRequested)
                {
                    TcpClient tcpClient = await _listener.AcceptTcpClientAsync();
                    ProcessTcpClient(tcpClient, stoppingToken);
                }
            }
            catch (SocketException ex)
            {
                string message = string.Format("Error listening on port {0}. Make sure IIS or another application is not running and consuming your port.", port);
                throw new Exception(message, ex);
            }
        }

        public void Dispose()
        {
            if (!_isDisposed)
            {
                _isDisposed = true;

                // safely attempt to shut down the listener
                try
                {
                    if (_listener != null)
                    {
                        if (_listener.Server != null)
                        {
                            _listener.Server.Close();
                        }

                        _listener.Stop();
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex.ToString());
                }

                _logger.LogInformation("Web Server disposed");
            }
        }
    }
}
